// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2001-2013, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.dev.test.util;

import com.ibm.icu.dev.test.CoreTestFmwk;
import com.ibm.icu.impl.ICULocaleService;
import com.ibm.icu.impl.ICULocaleService.ICUResourceBundleFactory;
import com.ibm.icu.impl.ICULocaleService.LocaleKey;
import com.ibm.icu.impl.ICULocaleService.LocaleKeyFactory;
import com.ibm.icu.impl.ICUNotifier;
import com.ibm.icu.impl.ICURWLock;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.ICUService;
import com.ibm.icu.impl.ICUService.Factory;
import com.ibm.icu.impl.ICUService.Key;
import com.ibm.icu.impl.ICUService.ServiceListener;
import com.ibm.icu.impl.ICUService.SimpleFactory;
import com.ibm.icu.impl.LocaleUtility;
import com.ibm.icu.util.ULocale;
import java.text.Collator;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ICUServiceTest extends CoreTestFmwk {
    private String lrmsg(String message, Object lhs, Object rhs) {
        return message + " lhs: " + lhs + " rhs: " + rhs;
    }

    public void confirmBoolean(String message, boolean val) {
        msg(message, val ? LOG : ERR, !val, true);
    }

    public void confirmEqual(String message, Object lhs, Object rhs) {
        msg(
                lrmsg(message, lhs, rhs),
                (lhs == null ? rhs == null : lhs.equals(rhs)) ? LOG : ERR,
                true,
                true);
    }

    public void confirmIdentical(String message, Object lhs, Object rhs) {
        msg(lrmsg(message, lhs, rhs), lhs == rhs ? LOG : ERR, true, true);
    }

    public void confirmIdentical(String message, int lhs, int rhs) {
        msg(message + " lhs: " + lhs + " rhs: " + rhs, lhs == rhs ? LOG : ERR, true, true);
    }

    /**
     * Convenience override of getDisplayNames(ULocale, Comparator, String) that uses the current
     * default ULocale as the locale, the default collator for the locale as the comparator to sort
     * the display names, and null for the matchID.
     */
    public SortedMap<String, String> getDisplayNames(ICUService service) {
        ULocale locale = ULocale.getDefault();
        Collator col = Collator.getInstance(locale.toLocale());
        return service.getDisplayNames(locale, col, null);
    }

    /**
     * Convenience override of getDisplayNames(ULocale, Comparator, String) that uses the default
     * collator for the locale as the comparator to sort the display names, and null for the
     * matchID.
     */
    public SortedMap<String, String> getDisplayNames(ICUService service, ULocale locale) {
        Collator col = Collator.getInstance(locale.toLocale());
        return service.getDisplayNames(locale, col, null);
    }

    /**
     * Convenience override of getDisplayNames(ULocale, Comparator, String) that uses the default
     * collator for the locale as the comparator to sort the display names.
     */
    public SortedMap<String, String> getDisplayNames(
            ICUService service, ULocale locale, String matchID) {
        Collator col = Collator.getInstance(locale.toLocale());
        return service.getDisplayNames(locale, col, matchID);
    }

    // use locale keys
    static final class TestService extends ICUService {
        public TestService() {
            super("Test Service");
        }

        @Override
        public Key createKey(String id) {
            return LocaleKey.createWithCanonicalFallback(id, null); // no fallback locale
        }
    }

    @Test
    public void TestAPI() {
        // create a service using locale keys,
        ICUService service = new TestService();

        logln("service name:" + service.getName());

        // register an object with one locale,
        // search for an object with a more specific locale
        // should return the original object
        Integer singleton0 = 0;
        service.registerObject(singleton0, "en_US");
        Object result = service.get("en_US_FOO");
        confirmIdentical("1) en_US_FOO -> en_US", result, singleton0);

        // register a new object with the more specific locale
        // search for an object with that locale
        // should return the new object
        Integer singleton1 = 1;
        service.registerObject(singleton1, "en_US_FOO");
        result = service.get("en_US_FOO");
        confirmIdentical("2) en_US_FOO -> en_US_FOO", result, singleton1);

        // search for an object that falls back to the first registered locale
        result = service.get("en_US_BAR");
        confirmIdentical("3) en_US_BAR -> en_US", result, singleton0);

        // get a list of the factories, should be two
        List<Factory> factories = service.factories();
        confirmIdentical("4) factory size", factories.size(), 2);

        // register a new object with yet another locale
        // original factory list is unchanged
        Integer singleton2 = 2;
        service.registerObject(singleton2, "en");
        confirmIdentical("5) factory size", factories.size(), 2);

        // search for an object with the new locale
        // stack of factories is now en, en_US_FOO, en_US
        // search for en_US should still find en_US object
        result = service.get("en_US_BAR");
        confirmIdentical("6) en_US_BAR -> en_US", result, singleton0);

        // register a new object with an old id, should hide earlier factory using this id, but
        // leave it there
        Integer singleton3 = 3;
        service.registerObject(singleton3, "en_US");
        factories = service.factories();
        confirmIdentical("9) factory size", factories.size(), 4);

        // should get data from that new factory
        result = service.get("en_US_BAR");
        confirmIdentical("10) en_US_BAR -> (3)", result, singleton3);

        // remove new factory
        // should have fewer factories again
        service.unregisterFactory((Factory) factories.get(0));
        factories = service.factories();
        confirmIdentical("11) factory size", factories.size(), 3);

        // should get original data again after remove factory
        result = service.get("en_US_BAR");
        confirmIdentical("12) en_US_BAR -> 0", result, singleton0);

        // shouldn't find unregistered ids
        result = service.get("foo");
        confirmIdentical("13) foo -> null", result, null);

        // should find non-canonical strings
        String[] resultID = new String[1];
        result = service.get("EN_us_fOo", resultID);
        confirmEqual("14) find non-canonical", resultID[0], "en_US_FOO");

        // should be able to register non-canonical strings and get them canonicalized
        service.registerObject(singleton3, "eN_ca_dUde");
        result = service.get("En_Ca_DuDe", resultID);
        confirmEqual("15) register non-canonical", resultID[0], "en_CA_DUDE");

        // should be able to register invisible factories, these will not
        // be visible by default, but if you know the secret password you
        // can still access these services...
        Integer singleton4 = 4;
        service.registerObject(singleton4, "en_US_BAR", false);
        result = service.get("en_US_BAR");
        confirmIdentical("17) get invisible", result, singleton4);

        // should not be able to locate invisible services
        Set<String> ids = service.getVisibleIDs();
        confirmBoolean("18) find invisible", !ids.contains("en_US_BAR"));

        service.reset();
        // an anonymous factory than handles all ids
        {
            Factory factory =
                    new Factory() {
                        @Override
                        public Object create(Key key, ICUService unusedService) {
                            return new ULocale(key.currentID());
                        }

                        @Override
                        public void updateVisibleIDs(Map<String, Factory> unusedResult) {}

                        @Override
                        public String getDisplayName(String id, ULocale l) {
                            return null;
                        }
                    };
            service.registerFactory(factory);

            // anonymous factory will still handle the id
            result = service.get(ULocale.US.toString());
            confirmEqual("21) locale", result, ULocale.US);

            // still normalizes id
            result = service.get("EN_US_BAR");
            confirmEqual("22) locale", result, new ULocale("en_US_BAR"));

            // we can override for particular ids
            service.registerObject(singleton3, "en_US_BAR");
            result = service.get("en_US_BAR");
            confirmIdentical("23) override super", result, singleton3);
        }

        // empty service should not recognize anything
        service.reset();
        result = service.get("en_US");
        confirmIdentical("24) empty", result, null);

        // create a custom multiple key factory
        {
            String[] xids = {
                "en_US_VALLEY_GIRL", "en_US_VALLEY_BOY", "en_US_SURFER_GAL", "en_US_SURFER_DUDE"
            };
            service.registerFactory(new TestLocaleKeyFactory(xids, "Later"));
        }

        // iterate over the visual ids returned by the multiple factory
        {
            Set<String> vids = service.getVisibleIDs();
            Iterator<String> iter = vids.iterator();
            int count = 0;
            while (iter.hasNext()) {
                ++count;
                String id = (String) iter.next();
                logln("  " + id + " --> " + service.get(id));
            }
            // four visible ids
            confirmIdentical("25) visible ids", count, 4);
        }

        // iterate over the display names
        {
            Map<String, String> dids = getDisplayNames(service, ULocale.GERMANY);
            Iterator<Map.Entry<String, String>> iter = dids.entrySet().iterator();
            int count = 0;
            while (iter.hasNext()) {
                ++count;
                Map.Entry<String, String> e = iter.next();
                logln("  " + e.getKey() + " -- > " + e.getValue());
            }
            // four display names, in german
            confirmIdentical("26) display names", count, 4);
        }

        // no valid display name
        confirmIdentical("27) get display name", service.getDisplayName("en_US_VALLEY_GEEK"), null);

        {
            String name = service.getDisplayName("en_US_SURFER_DUDE", ULocale.US);
            confirmEqual("28) get display name", name, "English (United States, SURFER_DUDE)");
        }

        // register another multiple factory
        {
            String[] xids = {
                "en_US_SURFER", "en_US_SURFER_GAL", "en_US_SILICON", "en_US_SILICON_GEEK"
            };
            service.registerFactory(new TestLocaleKeyFactory(xids, "Rad dude"));
        }

        // this time, we have seven display names
        // Rad dude's surfer gal 'replaces' later's surfer gal
        {
            Map<String, String> dids = getDisplayNames(service);
            Iterator<Map.Entry<String, String>> iter = dids.entrySet().iterator();
            int count = 0;
            while (iter.hasNext()) {
                ++count;
                Map.Entry<String, String> e = iter.next();
                logln("  " + e.getKey() + " --> " + e.getValue());
            }
            // seven display names, in spanish
            confirmIdentical("29) display names", count, 7);
        }

        // we should get the display name corresponding to the actual id
        // returned by the id we used.
        {
            String[] actualID = new String[1];
            String id = "en_us_surfer_gal";
            String gal = (String) service.get(id, actualID);
            if (gal != null) {
                logln("actual id: " + actualID[0]);
                String displayName = service.getDisplayName(actualID[0], ULocale.US);
                logln("found actual: " + gal + " with display name: " + displayName);
                confirmBoolean("30) found display name for actual", displayName != null);

                displayName = service.getDisplayName(id, ULocale.US);
                logln("found query: " + gal + " with display name: " + displayName);
                // this is no longer a bug, we want to return display names for anything
                // that a factory handles.  since we handle it, we should return a display
                // name.  see jb3549
                // confirmBoolean("31) found display name for query", displayName == null);
            } else {
                errln("30) service could not find entry for " + id);
            }

            // this should be handled by the 'dude' factory, since it overrides en_US_SURFER.
            id = "en_US_SURFER_BOZO";
            String bozo = (String) service.get(id, actualID);
            if (bozo != null) {
                String displayName = service.getDisplayName(actualID[0], ULocale.US);
                logln("found actual: " + bozo + " with display name: " + displayName);
                confirmBoolean("32) found display name for actual", displayName != null);

                displayName = service.getDisplayName(id, ULocale.US);
                logln("found actual: " + bozo + " with display name: " + displayName);
                // see above and jb3549
                // confirmBoolean("33) found display name for query", displayName == null);
            } else {
                errln("32) service could not find entry for " + id);
            }

            confirmBoolean("34) is default ", !service.isDefault());
        }

        /*
        // disallow hiding for now

        // hiding factory should obscure 'sublocales'
        {
        String[] xids = {
        "en_US_VALLEY", "en_US_SILICON"
        };
        service.registerFactory(new TestHidingFactory(xids, "hiding"));
        }

        {
        Map dids = service.getDisplayNames();
        Iterator iter = dids.entrySet().iterator();
        int count = 0;
        while (iter.hasNext()) {
        ++count;
        Entry e = (Entry)iter.next();
        logln("  " + e.getKey() + " -- > " + e.getValue());
        }
        confirmIdentical("35) hiding factory", count, 5);
        }
          */

        {
            Set<String> xids = service.getVisibleIDs();
            Iterator<String> iter = xids.iterator();
            while (iter.hasNext()) {
                String xid = (String) iter.next();
                logln(xid + "?  " + service.get(xid));
            }

            logln("valleygirl?  " + service.get("en_US_VALLEY_GIRL"));
            logln("valleyboy?   " + service.get("en_US_VALLEY_BOY"));
            logln("valleydude?  " + service.get("en_US_VALLEY_DUDE"));
            logln("surfergirl?  " + service.get("en_US_SURFER_GIRL"));
        }

        // resource bundle factory.
        service.reset();
        service.registerFactory(new ICUResourceBundleFactory());

        // list all of the resources
        {
            logln("all visible ids: " + service.getVisibleIDs());
            /*
            Set xids = service.getVisibleIDs();
            StringBuffer buf = new StringBuffer("{");
            boolean notfirst = false;
            Iterator iter = xids.iterator();
            while (iter.hasNext()) {
            String xid = (String)iter.next();
            if (notfirst) {
            buf.append(", ");
            } else {
            notfirst = true;
            }
            buf.append(xid);
            }
            buf.append("}");
            logln(buf.toString());
              */
        }

        // list only the resources for es, default locale
        // since we're using the default Key, only "es" is matched
        {
            logln("visible ids for es locale: " + service.getVisibleIDs("es"));
        }

        // list only the spanish display names for es, spanish collation order
        // since we're using the default Key, only "es" is matched
        {
            logln("display names: " + getDisplayNames(service, new ULocale("es"), "es"));
        }

        // list the display names in reverse order
        {
            logln(
                    "display names in reverse order: "
                            + service.getDisplayNames(
                                    ULocale.US,
                                    new Comparator<Object>() {
                                        @Override
                                        public int compare(Object lhs, Object rhs) {
                                            return -String.CASE_INSENSITIVE_ORDER.compare(
                                                    (String) lhs, (String) rhs);
                                        }
                                    }));
        }

        // get all the display names of these resources
        // this should be fast since the display names were cached.
        {
            logln("service display names for de_DE");
            Map<String, String> names = getDisplayNames(service, new ULocale("de_DE"));
            StringBuffer buf = new StringBuffer("{");
            Iterator<Map.Entry<String, String>> iter = names.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, String> e = iter.next();
                String name = e.getKey();
                String id = e.getValue();
                buf.append("\n   " + name + " --> " + id);
            }
            buf.append("\n}");
            logln(buf.toString());
        }

        CalifornioLanguageFactory califactory = new CalifornioLanguageFactory();
        service.registerFactory(califactory);
        // get all the display names of these resources
        {
            logln("californio language factory");
            StringBuffer buf = new StringBuffer("{");
            String[] idNames = {
                CalifornioLanguageFactory.californio,
                CalifornioLanguageFactory.valley,
                CalifornioLanguageFactory.surfer,
                CalifornioLanguageFactory.geek
            };
            for (int i = 0; i < idNames.length; ++i) {
                String idName = idNames[i];
                buf.append("\n  --- " + idName + " ---");
                Map<String, String> names = getDisplayNames(service, new ULocale(idName));
                Iterator<Map.Entry<String, String>> iter = names.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry<String, String> e = iter.next();
                    String name = e.getKey();
                    String id = e.getValue();
                    buf.append("\n    " + name + " --> " + id);
                }
            }
            buf.append("\n}");
            logln(buf.toString());
        }

        // test notification
        // simple registration
        {
            logln("simple registration notification");
            ICULocaleService ls = new ICULocaleService();
            ServiceListener l1 =
                    new ServiceListener() {
                        private int n;

                        @Override
                        public void serviceChanged(ICUService s) {
                            logln("listener 1 report " + n++ + " service changed: " + s);
                        }
                    };
            ls.addListener(l1);
            ServiceListener l2 =
                    new ServiceListener() {
                        private int n;

                        @Override
                        public void serviceChanged(ICUService s) {
                            logln("listener 2 report " + n++ + " service changed: " + s);
                        }
                    };
            ls.addListener(l2);
            logln("registering foo... ");
            ls.registerObject("Foo", "en_FOO");
            logln("registering bar... ");
            ls.registerObject("Bar", "en_BAR");
            logln("getting foo...");
            logln((String) ls.get("en_FOO"));
            logln("removing listener 2...");
            ls.removeListener(l2);
            logln("registering baz...");
            ls.registerObject("Baz", "en_BAZ");
            logln("removing listener 1");
            ls.removeListener(l1);
            logln("registering burp...");
            ls.registerObject("Burp", "en_BURP");

            // should only get one notification even if register multiple times
            logln("... trying multiple registration");
            ls.addListener(l1);
            ls.addListener(l1);
            ls.addListener(l1);
            ls.addListener(l2);
            ls.registerObject("Foo", "en_FOO");
            logln("... registered foo");

            // since in a separate thread, we can callback and not deadlock
            ServiceListener l3 =
                    new ServiceListener() {
                        private int n;

                        @Override
                        public void serviceChanged(ICUService s) {
                            logln("listener 3 report " + n++ + " service changed...");
                            if (s.get("en_BOINK") == null) { // don't recurse on ourselves!!!
                                logln("registering boink...");
                                s.registerObject("boink", "en_BOINK");
                            }
                        }
                    };
            ls.addListener(l3);
            logln("registering boo...");
            ls.registerObject("Boo", "en_BOO");
            logln("...done");

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
        }
    }

    static class TestLocaleKeyFactory extends LocaleKeyFactory {
        protected final Set<String> ids;
        protected final String factoryID;

        public TestLocaleKeyFactory(String[] ids, String factoryID) {
            super(VISIBLE, factoryID);

            this.ids = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(ids)));
            this.factoryID = factoryID + ": ";
        }

        @Override
        protected Object handleCreate(ULocale loc, int kind, ICUService service) {
            return factoryID + loc.toString();
        }

        @Override
        protected Set<String> getSupportedIDs() {
            return ids;
        }
    }

    /*
      // Disallow hiding for now since it causes gnarly problems, like
      // how do you localize the hidden (but still exported) names.

      static class TestHidingFactory implements ICUService.Factory {
      protected final String[] ids;
      protected final String factoryID;

      public TestHidingFactory(String[] ids) {
      this(ids, "Hiding");
      }

      public TestHidingFactory(String[] ids, String factoryID) {
      this.ids = (String[])ids.clone();

      if (factoryID == null || factoryID.length() == 0) {
      this.factoryID = "";
      } else {
      this.factoryID = factoryID + ": ";
      }
      }

      public Object create(Key key, ICUService service) {
      for (int i = 0; i < ids.length; ++i) {
      if (LocaleUtility.isFallbackOf(ids[i], key.currentID())) {
      return factoryID + key.canonicalID();
      }
      }
      return null;
      }

      public void updateVisibleIDs(Map result) {
      for (int i = 0; i < ids.length; ++i) {
      String id = ids[i];
      Iterator iter = result.keySet().iterator();
      while (iter.hasNext()) {
      if (LocaleUtility.isFallbackOf(id, (String)iter.next())) {
      iter.remove();
      }
      }
      result.put(id, this);
      }
      }

      public String getDisplayName(String id, ULocale locale) {
      return factoryID + new ULocale(id).getDisplayName(locale);
      }
      }
    */

    static class CalifornioLanguageFactory extends ICUResourceBundleFactory {
        public static String californio = "en_US_CA";
        public static String valley = californio + "_VALLEY";
        public static String surfer = californio + "_SURFER";
        public static String geek = californio + "_GEEK";
        public static Set<String> supportedIDs;

        static {
            HashSet<String> result = new HashSet<>();
            result.addAll(ICUResourceBundle.getAvailableLocaleNameSet());
            result.add(californio);
            result.add(valley);
            result.add(surfer);
            result.add(geek);
            supportedIDs = Collections.unmodifiableSet(result);
        }

        @Override
        public Set<String> getSupportedIDs() {
            return supportedIDs;
        }

        @Override
        public String getDisplayName(String id, ULocale locale) {
            String prefix = "";
            String suffix = "";
            String ls = locale.toString();
            if (LocaleUtility.isFallbackOf(californio, ls)) {
                if (ls.equalsIgnoreCase(valley)) {
                    prefix = "Like, you know, it's so totally ";
                } else if (ls.equalsIgnoreCase(surfer)) {
                    prefix = "Dude, its ";
                } else if (ls.equalsIgnoreCase(geek)) {
                    prefix = "I'd estimate it's approximately ";
                } else {
                    prefix = "Huh?  Maybe ";
                }
            }
            if (LocaleUtility.isFallbackOf(californio, id)) {
                if (id.equalsIgnoreCase(valley)) {
                    suffix = "like the Valley, you know?  Let's go to the mall!";
                } else if (id.equalsIgnoreCase(surfer)) {
                    suffix = "time to hit those gnarly waves, Dude!!!";
                } else if (id.equalsIgnoreCase(geek)) {
                    suffix = "all systems go.  T-Minus 9, 8, 7...";
                } else {
                    suffix = "No Habla Englais";
                }
            } else {
                suffix = super.getDisplayName(id, locale);
            }

            return prefix + suffix;
        }
    }

    @Test
    public void TestLocale() {
        ICULocaleService service = new ICULocaleService("test locale");
        service.registerObject("root", ULocale.ROOT);
        service.registerObject("german", "de");
        service.registerObject("german_Germany", ULocale.GERMANY);
        service.registerObject("japanese", "ja");
        service.registerObject("japanese_Japan", ULocale.JAPAN);

        Object target = service.get("de_US");
        confirmEqual("test de_US", "german", target);

        ULocale de = new ULocale("de");
        ULocale de_US = new ULocale("de_US");

        target = service.get(de_US);
        confirmEqual("test de_US 2", "german", target);

        target = service.get(de_US, LocaleKey.KIND_ANY);
        confirmEqual("test de_US 3", "german", target);

        target = service.get(de_US, 1234);
        confirmEqual("test de_US 4", "german", target);

        ULocale[] actualReturn = new ULocale[1];
        target = service.get(de_US, actualReturn);
        confirmEqual("test de_US 5", "german", target);
        confirmEqual("test de_US 6", actualReturn[0], de);

        actualReturn[0] = null;
        target = service.get(de_US, LocaleKey.KIND_ANY, actualReturn);
        confirmEqual("test de_US 7", actualReturn[0], de);

        actualReturn[0] = null;
        target = service.get(de_US, 1234, actualReturn);
        confirmEqual("test de_US 8", "german", target);
        confirmEqual("test de_US 9", actualReturn[0], de);

        service.registerObject("one/de_US", de_US, 1);
        service.registerObject("two/de_US", de_US, 2);

        target = service.get(de_US, 1);
        confirmEqual("test de_US kind 1", "one/de_US", target);

        target = service.get(de_US, 2);
        confirmEqual("test de_US kind 2", "two/de_US", target);

        target = service.get(de_US);
        confirmEqual("test de_US kind 3", "german", target);

        LocaleKey lkey = LocaleKey.createWithCanonicalFallback("en", null, 1234);
        logln("lkey prefix: " + lkey.prefix());
        logln("lkey descriptor: " + lkey.currentDescriptor());
        logln("lkey current locale: " + lkey.currentLocale());

        lkey.fallback();
        logln("lkey descriptor 2: " + lkey.currentDescriptor());

        lkey.fallback();
        logln("lkey descriptor 3: " + lkey.currentDescriptor());

        target = service.get("za_PPP");
        confirmEqual("test zappp", "root", target);

        ULocale loc = ULocale.getDefault();
        ULocale.setDefault(ULocale.JAPANESE);
        target = service.get("za_PPP");
        confirmEqual("test with ja locale", "japanese", target);

        for (String id : service.getVisibleIDs()) {
            logln("id: " + id);
        }

        ULocale.setDefault(loc);
        for (String id : service.getVisibleIDs()) {
            logln("id: " + id);
        }

        target = service.get("za_PPP");
        confirmEqual("test with en locale", "root", target);

        ULocale[] locales = service.getAvailableULocales();
        confirmIdentical("test available locales", locales.length, 6);
        logln("locales: ");
        for (int i = 0; i < locales.length; ++i) {
            log("\n  [" + i + "] " + locales[i]);
        }
        logln(" ");

        service.registerFactory(new ICUResourceBundleFactory());
        target = service.get(ULocale.JAPAN);

        {
            int n = 0;
            List<Factory> factories = service.factories();
            Iterator<Factory> iter = factories.iterator();
            while (iter.hasNext()) {
                logln("[" + n++ + "] " + iter.next());
            }
        }

        // list only the english display names for es, in reverse order
        // since we're using locale keys, we should get all and only the es locales
        // hmmm, the default toString function doesn't print in sorted order for TreeMap
        {
            SortedMap<String, String> map =
                    service.getDisplayNames(
                            ULocale.US,
                            new Comparator<Object>() {
                                @Override
                                public int compare(Object lhs, Object rhs) {
                                    return -String.CASE_INSENSITIVE_ORDER.compare(
                                            (String) lhs, (String) rhs);
                                }
                            },
                            "es");

            logln("es display names in reverse order " + map);
        }
    }

    @Test
    public void TestWrapFactory() {
        final String greeting = "Hello There";
        final String greetingID = "greeting";

        ICUService service = new ICUService("wrap");
        service.registerObject(greeting, greetingID);

        logln("test one: " + service.get(greetingID));

        class WrapFactory implements Factory {
            @Override
            public Object create(Key key, ICUService serviceArg) {
                if (key.currentID().equals(greetingID)) {
                    Object previous = serviceArg.getKey(key, null, this);
                    return "A different greeting: \"" + previous + "\"";
                }
                return null;
            }

            @Override
            public void updateVisibleIDs(Map<String, Factory> result) {
                result.put("greeting", this);
            }

            @Override
            public String getDisplayName(String id, ULocale locale) {
                return "wrap '" + id + "'";
            }
        }
        service.registerFactory(new WrapFactory());

        confirmEqual(
                "wrap test: ",
                service.get(greetingID),
                "A different greeting: \"" + greeting + "\"");
    }

    // misc coverage tests
    @Test
    public void TestCoverage() {
        // Key
        Key key = new Key("foobar");
        logln("ID: " + key.id());
        logln("canonicalID: " + key.canonicalID());
        logln("currentID: " + key.currentID());
        logln("has fallback: " + key.fallback());

        // SimpleFactory
        Object obj = new Object();
        SimpleFactory sf = new SimpleFactory(obj, "object");
        try {
            sf = new SimpleFactory(null, null);
            errln("didn't throw exception");
        } catch (IllegalArgumentException e) {
            logln("OK: " + e.getMessage());
        } catch (Exception e) {
            errln("threw wrong exception" + e);
        }
        logln(sf.getDisplayName("object", null));

        // ICUService
        ICUService service = new ICUService();
        service.registerFactory(sf);

        try {
            service.get(null, null);
            errln("didn't throw exception");
        } catch (NullPointerException e) {
            logln("OK: " + e.getMessage());
        }
        /*
        catch (Exception e) {
        errln("threw wrong exception" + e);
        }
          */
        try {
            service.registerFactory(null);
            errln("didn't throw exception");
        } catch (NullPointerException e) {
            logln("OK: " + e.getMessage());
        } catch (Exception e) {
            errln("threw wrong exception" + e);
        }

        try {
            service.unregisterFactory(null);
            errln("didn't throw exception");
        } catch (NullPointerException e) {
            logln("OK: " + e.getMessage());
        } catch (Exception e) {
            errln("threw wrong exception" + e);
        }

        logln("object is: " + service.get("object"));

        logln("stats: " + service.stats());

        // ICURWLock

        ICURWLock rwlock = new ICURWLock();
        rwlock.resetStats();

        rwlock.acquireRead();
        rwlock.releaseRead();

        rwlock.acquireWrite();
        rwlock.releaseWrite();
        logln("stats: " + rwlock.getStats());
        logln("stats: " + rwlock.clearStats());
        rwlock.acquireRead();
        rwlock.releaseRead();
        rwlock.acquireWrite();
        rwlock.releaseWrite();
        logln("stats: " + rwlock.getStats());

        try {
            rwlock.releaseRead();
            errln("no error thrown");
        } catch (Exception e) {
            logln("OK: " + e.getMessage());
        }

        try {
            rwlock.releaseWrite();
            errln("no error thrown");
        } catch (Exception e) {
            logln("OK: " + e.getMessage());
        }

        // ICULocaleService

        // LocaleKey

        // LocaleKey lkey = LocaleKey.create("en_US", "ja_JP");
        // lkey = LocaleKey.create(null, null);
        LocaleKey lkey = LocaleKey.createWithCanonicalFallback("en_US", "ja_JP");
        logln("lkey: " + lkey);

        lkey = LocaleKey.createWithCanonicalFallback(null, null);
        logln("lkey from null,null: " + lkey);

        // LocaleKeyFactory
        LocaleKeyFactory lkf = new LKFSubclass(false);
        logln("lkf: " + lkf);
        logln("obj: " + lkf.create(lkey, null));
        logln(lkf.getDisplayName("foo", null));
        logln(lkf.getDisplayName("bar", null));
        lkf.updateVisibleIDs(new HashMap<>());

        LocaleKeyFactory invisibleLKF = new LKFSubclass(false);
        logln("obj: " + invisibleLKF.create(lkey, null));
        logln(invisibleLKF.getDisplayName("foo", null));
        logln(invisibleLKF.getDisplayName("bar", null));
        invisibleLKF.updateVisibleIDs(new HashMap<>());

        // ResourceBundleFactory
        ICUResourceBundleFactory rbf = new ICUResourceBundleFactory();
        logln("RB: " + rbf.create(lkey, null));

        // ICUNotifier
        ICUNotifier nf = new ICUNSubclass();
        try {
            nf.addListener(null);
            errln("added null listener");
        } catch (NullPointerException e) {
            logln(e.getMessage());
        } catch (Exception e) {
            errln("got wrong exception");
        }

        try {
            nf.addListener(new WrongListener());
            errln("added wrong listener");
        } catch (IllegalStateException e) {
            logln(e.getMessage());
        } catch (Exception e) {
            errln("got wrong exception");
        }

        try {
            nf.removeListener(null);
            errln("removed null listener");
        } catch (NullPointerException e) {
            logln(e.getMessage());
        } catch (Exception e) {
            errln("got wrong exception");
        }

        nf.removeListener(new MyListener());
        nf.notifyChanged();
        nf.addListener(new MyListener());
        nf.removeListener(new MyListener());
    }

    static class MyListener implements EventListener {}

    static class WrongListener implements EventListener {}

    static class ICUNSubclass extends ICUNotifier {
        @Override
        public boolean acceptsListener(EventListener l) {
            return l instanceof MyListener;
        }

        // not used, just needed to implement abstract base
        @Override
        public void notifyListener(EventListener l) {}
    }

    static class LKFSubclass extends LocaleKeyFactory {
        LKFSubclass(boolean visible) {
            super(visible ? VISIBLE : INVISIBLE);
        }

        @Override
        protected Set<String> getSupportedIDs() {
            return Collections.emptySet();
        }
    }
}
